iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0

軟體開發的複雜性應該是軟體開發的眾多特性裡,最不需要特別証明的特性。隨便一個 5~10 行的函數,如果沒有寫好的話,都有可能讓工程師覺得難以理解。

而工程師有哪些方式可以理解程式碼呢?

主要也只有兩種方式:透過「閱讀原始碼」或「以黑箱方式觀察輸入/輸出行為」來理解,這分別對應到《走出焦油坑》(Out of the Tar Pit) 一文中的「非正式推理」和「測試」。

由於 Lisp 語言提供了互動式開發,這等同於是讓工程師可以隨意地對任意區段的程式碼進行測試,自然是對工程師理解複雜程式碼帶來極大的助益。

另一方面,若考慮「閱讀原始碼」作為理解方式的話,程式碼的數量、變動的狀態、還有不直觀的邏輯、不恰當的命名等,都會有效地增加複雜度,進而讓工程師感到痛苦。

應對之道

閱讀特定區段程式碼,工程師在尋找什麼答案,十之八九,他們在設法理解:

  1. 它是什麼?(what)
  2. 它是如何實現的?(how)

基於這個觀察,我們可以推論,如果程式碼撰寫的方式,可以有效地將「是什麼」放在上層、「如何實現」放在下層,那工程師理解程式碼的負擔將可以有效地降低。

因為當工程師要理解「是什麼」時,他可以讀上層的程式碼就好,這邊的程式碼數量將會少很多。當工程師要理解「如何實現」時,他還可以先到上層的程式碼選出他最在意的區段,再去尋找對應的實現方式。換言之,將 what/how 分離,就是一個讓理解可以分而治之的有效策略,而此一策略在軟體開發裡,也有諸多的體現。

在日常實踐中,有一個經驗法則可以幫助我們起步:「相同的東西重覆出現三次,就將其提取出來,做成函數。」這個經驗法則簡單可操作,重覆出現也很有可能暗示了該區段程式碼可能與「如何實現」的概念一致,應該被包裹起來。然而,要真正做到將 what/how 分離,光靠這個機械性的「三次」法則是不夠的,仍然需要有意識地思考「是什麼?」與「如何實現?」兩者的差異,並主動設計抽象層次。

Container/Presenter Pattern

Container/Presenter Pattern 是 React 前端開發中常見的一種設計手法,核心思想是將「做什麼」與「怎麼做」分離,讓程式碼結構更清晰,也更容易維護。

  • Container (容器)
    負責「要顯示什麼」以及「互動發生後要做什麼」。它管理狀態,處理邏輯,並決定哪些事件需要被觸發。換言之,Container 回答的是 What to display 的問題。

  • Presenter (展示者)
    專注於「畫面該怎麼顯示」,它僅根據 props 繪製 UI,並把事件交由注入的 handlers 處理;至於資料來源與事件語義,它一概不關心。Presenter 回答的是 How to display 的問題。

當一個複雜 UI 被拆分成這兩個角色後,what/how 分離的好處就很直觀:

  • 想理解「畫面需要哪些資料、點擊後發生什麼」——看 Container。
  • 想理解「這些資料如何呈現、按鈕長什麼樣子」——看 Presenter。

這種分離讓每個元件的職責都變得單純,從而大大降低了程式碼的理解難度。

DSL + Interpreter Pattern

Domain Specific Language (DSL) 和 Interpreter Pattern 的組合,是 what/how 分離的另一個經典體現。這就像是設計一種專門的語言(DSL)來描述問題,然後再用一個翻譯機(Interpreter)來執行它。

Tree-sitter 是一個用於程式碼語法解析的函式庫,它就很好地實踐了這個概念。

  • what(查詢語言):Tree-sitter 提供了強大的查詢語言,讓你用類似於 CSS Selector 的語法來描述你想要從抽象語法樹(AST)中找到什麼。例如,你可能寫下 (function_declaration name: (identifier) @name) 來描述「我想要找到所有的函式宣告,並擷取其名稱」。這個查詢本身就是一種 DSL,它清晰地定義了你要什麼,而不需要關心底層的搜尋演算法。

  • how(執行引擎):Tree-sitter 的核心引擎負責解釋和執行你的查詢。它知道如何遍歷語法樹、如何比對節點類型、如何處理命名捕獲(@name)等等。對使用者來說,當查詢語言總是正常運作時,使用者可以完全忽略執行引擎的實作。

換句話說,當你使用 Tree-sitter 時,你只專注於描述你的需求(what),也就是你想要從程式碼中提取出什麼資訊。至於這個需求如何被實現 (how),則完全交由底層的引擎來處理。這種模式讓你可以只用幾行查詢,就輕鬆地完成複雜的程式碼分析任務,而不需要深入了解底層的演算法細節。

相關理論

這種將複雜系統分解成更小、更易於管理的子系統,並以階層方式組織的模式,與 Herbert Alexander Simon 在其著作 The Architecture of Complexity 中提出的層級複雜性(hierarchical complexity)概念不謀而合。

Simon 透過一個著名的故事——兩個製錶師 Hora 和 Tempus——來闡述這個概念。Hora 的製錶方式是先將所有的零件組合起來,然後再將它們全部固定,但他常常在完成前被打斷,導致所有的工作都必須從頭開始。Tempus 則採用了不同的方法,他先將零件組成小型的子單元,然後再將這些子單元組合成更大的子單元,最後才將它們組合成一個完整的時鐘。當他被打斷時,他只需要重新組合最後完成的子單元即可,而不必從頭來過。

在這個比喻中,Tempus 的工作方式代表了「可分解的階層系統」(decomposable hierarchical system)。這類系統由獨立或幾乎獨立的子系統構成,每個子系統又由更小的子系統構成,如此層層遞進。這種結構使得在單一子系統內的變動,不太會影響到其他子系統。

將這個概念應用到軟體開發中,我們可以看出,what/how 分離就是一種建立階層系統的有效方法。在程式碼中,what(是什麼)位於抽象層級較高的位置,它描述了功能的目標,而how(如何實現)則位於較低的層級,它包含了具體的實作細節。

這使得我們可以像 Tempus 一樣,在單一的層級上進行修改和理解,而不必理解整個系統的龐大細節。當我們想要理解一個功能時,可以先從 what 的層級入手,這就像是審視一個時鐘的其中一個「子單元」;如果需要更深入地了解其運作原理,再向下深入到 how 的層級。這種階層式組織不僅讓程式碼更容易被理解,也讓維護和修改變得更有效率,因為變更的影響範圍被限制在特定的子系統內。

小結

複雜性是軟體開發裡最廣為人知的挑戰,但是應對之道則相對不那麼廣為人所知。本文介紹核心的「what/how 分離」這個一般模式,並且舉了兩個子模式:

  • Container/Presenter Pattern
  • DSL + Interpreter Pattern

這些模式同時也與 Herbert Alexander Simon 所提出的階層式複雜性理論不謀而合。儘管實務上我們可以依循「相同的東西重覆出現三次,就將其提取出來」的簡單法則來邁出第一步,但要達成真正的 what/how 分離,仍需要設計者有意識地進行抽象。這些模式造成的結果都是:需要修改或理解時,只需關注特定層級即可,無需從頭來過。


上一篇
模式與原理—不確定性
下一篇
模式與原理—修改傳播
系列文
在 Neovim 中探索 Fennel 與函數式編程30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言